掌握 React Server Action 验证。深入探讨表单处理、安全最佳实践,以及使用 Zod、useFormState 和 useFormStatus 的高级技巧。
React Server Action 验证:表单输入处理与安全的综合指南
React Server Actions 的引入标志着使用 Next.js 等框架进行全栈开发的重大范式转变。通过允许客户端组件直接调用服务器端函数,我们现在可以用更少的样板代码构建更具凝聚力、更高效、更具交互性的应用程序。然而,这个强大的新抽象将一个至关重要的责任推到了最前沿:强大的输入验证和安全性。
当客户端和服务器之间的界限变得如此无缝时,我们很容易忽视 Web 安全的基本原则。任何来自用户的输入都是不可信的,必须在服务器上进行严格验证。本指南全面探讨了 React Server Actions 中的表单输入处理和验证,涵盖了从基本原则到高级、生产就绪的模式,确保您的应用程序既用户友好又安全。
究竟什么是 React Server Actions?
在深入探讨验证之前,让我们简要回顾一下什么是 Server Actions。从本质上讲,它们是您在服务器上定义但可以从客户端执行的函数。当用户提交表单或点击按钮时,可以直接调用 Server Action,无需手动创建 API 端点、处理 `fetch` 请求以及管理加载/错误状态。
它们建立在 HTML 表单和 Web 平台的 `FormData` API 的基础上,使其默认具有渐进增强的特性。这意味着即使 JavaScript 加载失败,您的表单也能正常工作,从而提供有弹性的用户体验。
一个基础 Server Action 的示例:
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// ... logic to save user to the database
console.log('Creating user:', { name, email });
}
// app/page.js
import { createUser } from './actions';
export default function UserForm() {
return (
);
}
这种简洁性非常强大,但它也隐藏了背后发生的事情的复杂性。`createUser` 函数完全在服务器上运行,但它是从客户端组件调用的。正是这条直通您服务器逻辑的线路,使得验证不仅仅是一个功能——它是一项必需的要求。
验证的坚定重要性
在 Server Actions 的世界里,每个函数都是通往您服务器的一扇敞开的大门。适当的验证就像是门口的守卫。以下是为什么它不容商榷的原因:
- 数据完整性: 您的数据库和应用程序状态依赖于干净、可预测的数据。验证确保您不会存储格式错误的电子邮件地址、本应是姓名的空字符串,或在数字字段中存储文本。
- 增强的用户体验 (UX): 用户会犯错。清晰、即时且针对特定上下文的错误消息会引导他们纠正输入,减少挫败感并提高表单完成率。
- 铁甲般的安全: 这是最关键的方面。没有服务器端验证,您的应用程序将容易受到多种攻击,包括:
- SQL 注入: 恶意行为者可能会在表单字段中提交 SQL 命令来操纵您的数据库。
- 跨站脚本 (XSS): 如果您存储并渲染未经清理的用户输入,攻击者可能会注入恶意脚本,并在其他用户的浏览器中执行。
- 拒绝服务 (DoS): 提交意想不到的大量或计算成本高昂的数据可能会耗尽您的服务器资源。
客户端验证 vs. 服务器端验证:必要的伙伴关系
理解验证应该在两个地方进行非常重要:
- 客户端验证: 这是为了用户体验。它提供即时反馈,无需网络往返。您可以使用简单的 HTML5 属性,如 `required`、`minLength`、`pattern`,或使用 JavaScript 在用户输入时检查格式。然而,它很容易通过禁用 JavaScript 或使用开发者工具被绕过。
- 服务器端验证: 这是为了安全和数据完整性。它是您应用程序的唯一真实来源。无论客户端发生什么,服务器都必须重新验证它收到的所有内容。Server Actions 是实现此逻辑的完美场所。
经验法则: 使用客户端验证以获得更好的用户体验,但始终只信任服务器端验证来保障安全。
在 Server Actions 中实现验证:从基础到高级
让我们逐步建立我们的验证策略,从简单的方法开始,然后转向使用现代工具的更强大、可扩展的解决方案。
方法 1:手动验证并返回状态
处理验证最简单的方法是在您的 Server Action 中添加 `if` 语句,并返回一个指示成功或失败的对象。
// app/actions.js
'use server';
import { redirect } from 'next/navigation';
export async function createInvoice(formData) {
const customerName = formData.get('customerName');
const amount = formData.get('amount');
if (!customerName || customerName.trim() === '') {
return { success: false, message: 'Customer name is required.' };
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
return { success: false, message: 'Please enter a valid amount greater than zero.' };
}
// ... logic to create the invoice in the database
console.log('Invoice created for', customerName, 'with amount', amount);
redirect('/dashboard/invoices');
}
这种方法可行,但它有一个主要的用户体验缺陷:它需要完整的页面重新加载才能显示错误消息。我们无法轻易地在表单页面上显示消息。这就是 React 专为 Server Actions 设计的钩子发挥作用的地方。
方法 2:使用 `useFormState` 实现无缝错误处理
`useFormState` 钩子就是专门为此目的而设计的。它允许 Server Action 返回可用于更新 UI 的状态,而无需完整的导航事件。它是使用 Server Actions 进行现代表单处理的基石。
让我们重构我们的发票创建表单。
步骤 1:更新 Server Action
该 action 现在需要接受两个参数:`prevState` 和 `formData`。它应该返回一个新的状态对象,`useFormState` 将用它来更新组件。
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// Define the initial state shape
const initialState = {
message: null,
errors: {},
};
export async function createInvoice(prevState, formData) {
const customerName = formData.get('customerName');
const amount = formData.get('amount');
const status = formData.get('status');
const errors = {};
if (!customerName || customerName.trim().length < 2) {
errors.customerName = 'Customer name must be at least 2 characters.';
}
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
errors.amount = 'Please enter a valid amount.';
}
if (status !== 'pending' && status !== 'paid') {
errors.status = 'Please select a valid status.';
}
if (Object.keys(errors).length > 0) {
return {
message: 'Failed to create invoice. Please check the fields.',
errors,
};
}
try {
// ... logic to save to database
console.log('Invoice created successfully!');
} catch (e) {
return {
message: 'Database Error: Failed to create invoice.',
errors: {},
};
}
// Revalidate the cache for the invoices page and redirect
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
步骤 2:使用 `useFormState` 更新表单组件
在我们的客户端组件中,我们将使用这个钩子来管理表单的状态并显示错误。
// app/ui/invoices/create-form.js
'use client';
import { useFormState } from 'react-dom';
import { createInvoice } from '@/app/actions';
const initialState = {
message: null,
errors: {},
};
export function CreateInvoiceForm() {
const [state, dispatch] = useFormState(createInvoice, initialState);
return (
);
}
现在,当用户提交无效表单时,Server Action 会运行,返回错误对象,然后 `useFormState` 更新 `state` 变量。组件会重新渲染,将具体的错误消息直接显示在相应字段旁边——所有这些都无需页面重新加载。这是一个巨大的用户体验改进!
方法 3:使用 `useFormStatus` 增强用户体验
在 Server Action 运行时会发生什么?用户可能会多次点击提交按钮。我们可以使用 `useFormStatus` 钩子来提供反馈,它为我们提供了上次表单提交状态的信息。
重要提示:`useFormStatus` 必须在作为 `